import itertools
import logging
import typing
from typing import Any, Dict, Hashable, List, Optional, Set, Text, Tuple, Type, Iterable

from rasa.constants import DOCS_URL_MIGRATION_GUIDE
from rasa.nlu.constants import TRAINABLE_EXTRACTORS
from rasa.nlu.config import RasaNLUModelConfig, override_defaults, InvalidConfigError
from rasa.nlu.training_data import Message, TrainingData
from rasa.utils.common import raise_warning

if typing.TYPE_CHECKING:
    from rasa.nlu.model import Metadata

logger = logging.getLogger(__name__)


def find_unavailable_packages(package_names: List[Text]) -> Set[Text]:
    """尝试导入所有软件包名称，并返回失败的软件包。

    Args:
        package_names: 要导入的软件包名称。
    Returns:
        无法导入的软件包名称。
    """
    import importlib

    failed_imports = set()   # 返回失败的包
    for package in package_names:
        try:
            importlib.import_module(package)
        except ImportError:
            failed_imports.add(package)
    return failed_imports


def validate_requirements(component_names: List[Text]) -> None:
    """验证是否已安装所有必需的可导入python软件包。
    Args:
        component_names: 组件名称的列表。
    """
    from rasa.nlu import registry

    # 验证是否已安装所有必需的软件包
    failed_imports = set()
    for component_name in component_names:
        component_class = registry.get_component_class(component_name)
        failed_imports.update(
            find_unavailable_packages(component_class.required_packages())
        )
    if failed_imports:
        # pragma: no cover
        # 如果有fail的包，请使用开发文件找出每种要求的正确版本号
        raise Exception(
            f"Not all required importable packages are installed. "
            f"To use this pipeline, you need to install the "
            f"missing dependencies. "
            f"Please install the package(s) that contain the module(s): "
            f"{', '.join(failed_imports)}"
        )


def validate_empty_pipeline(pipeline: List["Component"]) -> None:
    """确保管道（pipeline）不为空
    Args:
        pipeline: lass:`rasa.nlu.components.Component`的列表
    """

    if len(pipeline) == 0:
        raise InvalidConfigError(
            "Can not train an empty pipeline. "
            "Make sure to specify a proper pipeline in "
            "the configuration using the 'pipeline' key. "
            "The 'backend' configuration key is "
            "NOT supported anymore."
        )


def validate_only_one_tokenizer_is_used(pipeline: List["Component"]) -> None:
    """验证管道中仅存在一个tokenizer。
    Args:
        pipeline: rasa.nlu.components.Component的类列表。
    """
    from rasa.nlu.tokenizers.tokenizer import Tokenizer

    tokenizer_names = []
    for component in pipeline:
        if isinstance(component, Tokenizer):
            tokenizer_names.append(component.name)

    if len(tokenizer_names) > 1:
        raise InvalidConfigError(
            f"More than one tokenizer is used: {tokenizer_names}. "
            f"You can use only one tokenizer."
        )


def _required_component_in_pipeline(
    required_component: Type["Component"], pipeline: List["Component"]
) -> bool:
    """检查管道中是否存在必需的组件，即先前组件中是否存在所必须的组件。
    Args:
        required_component: 必需组件的对象名称。
        pipeline:           rasa.nlu.components.Component的类列表。
    Returns:
        如果required_component在管道中，则为True，否则为False
    """

    for previous_component in pipeline:
        if isinstance(previous_component, required_component):
            return True
    return False


def _check_deprecated_attributes(component: "Component") -> None:
    """检查确保组件没有包含不推荐使用的属性，主要是为了版本升级后的维护。

    Args:
        component: The :class:`rasa.nlu.components.Component`.
    """

    if hasattr(component, "provides"):
        raise_warning(
            f"'{component.name}' contains property 'provides', "
            f"which is deprecated. There is no need to specify "
            f"the list of attributes that a component provides.",
            category=FutureWarning,
            docs=DOCS_URL_MIGRATION_GUIDE,
        )
    if hasattr(component, "requires"):
        raise_warning(
            f"'{component.name}' contains property 'requires', "
            f"which is deprecated. Use 'required_components()' method "
            f"to specify which components are required to be present "
            f"in the pipeline by this component.",
            category=FutureWarning,
            docs=DOCS_URL_MIGRATION_GUIDE,
        )


def validate_required_components(pipeline: List["Component"]) -> None:
    """验证管道中是否存在所有必需的组件。
    Args:
        pipeline: rasa.nlu.components.Component的列表。
    """

    for i, component in enumerate(pipeline):

        # 检查组件是否包含不推荐使用的属性和方法
        _check_deprecated_attributes(component)

        # 检查pipline中是否存在当前组件所必须的先前组件
        missing_components = []
        for required_component in component.required_components():
            if not _required_component_in_pipeline(required_component, pipeline[:i]):
                missing_components.append(required_component.name)

        if missing_components:
            raise InvalidConfigError(
                f"'{component.name}' requires {missing_components}. "
                f"Add required components to the pipeline."
            )


def validate_pipeline(pipeline: List["Component"]) -> None:
    """验证管道类组件对象的有效性。
    Args:
        pipeline: class:`rasa.nlu.components.Component`的列表
    """

    validate_empty_pipeline(pipeline)
    validate_only_one_tokenizer_is_used(pipeline)
    validate_required_components(pipeline)


def any_components_in_pipeline(components: Iterable[Text], pipeline: List["Component"]):
    """检查管道中是否列出了所有提供的组件。

    Args:
        components:  要检查的rasa.nlu.components.Component的列表。
        pipeline:   rasa.nlu.components.Component`s的列表.
    Returns:
        如果组件中的任何一个都在管道中，则为True；否则为False。.
    """
    return any(any([component.name == c for component in pipeline]) for c in components)


def validate_required_components_from_data(
    pipeline: List["Component"], data: TrainingData
) -> None:
    """根据数据验证管道中是否存在所有组件。
    Args:
        pipeline: rasa.nlu.components.Component的列表。
        data:     The :class:`rasa.nlu.training_data.training_data.TrainingData`.
    """

    if data.response_examples and not any_components_in_pipeline(
        ["ResponseSelector"], pipeline
    ):
        raise_warning(
            "You have defined training data with examples for training a response selector, but "
            "your NLU pipeline does not include a response selector component. To train a "
            "model on your response selector data, add a 'ResponseSelector' to your pipeline."
        )

    if data.entity_examples and not any_components_in_pipeline(
        TRAINABLE_EXTRACTORS, pipeline
    ):
        raise_warning(
            "You have defined training data consisting of entity examples, but "
            "your NLU pipeline does not include an entity extractor trained on "
            "your training data. To extract non-pretrained entities, add one of "
            f"{TRAINABLE_EXTRACTORS} to your pipeline."
        )

    if data.regex_features and not any_components_in_pipeline(
        ["RegexFeaturizer"], pipeline
    ):
        raise_warning(
            "You have defined training data with regexes, but "
            "your NLU pipeline does not include a 'RegexFeaturizer'. "
            "To featurize regexes, include a 'RegexFeaturizer' in your pipeline."
        )

    if data.lookup_tables and not any_components_in_pipeline(
        ["RegexFeaturizer"], pipeline
    ):
        raise_warning(
            "You have defined training data consisting of lookup tables, but "
            "your NLU pipeline does not include a 'RegexFeaturizer'. "
            "To featurize lookup tables, add a 'RegexFeaturizer' to your pipeline."
        )

    if data.lookup_tables:
        if not any_components_in_pipeline(
            ["CRFEntityExtractor", "DIETClassifier"], pipeline
        ):
            raise_warning(
                "You have defined training data consisting of lookup tables, but "
                "your NLU pipeline does not include any components that use these features. "
                "To make use of lookup tables, add a 'DIETClassifier' or a 'CRFEntityExtractor' with "
                "the 'pattern' feature to your pipeline."
            )
        elif any_components_in_pipeline(["CRFEntityExtractor"], pipeline):
            crf_components = [c for c in pipeline if c.name == "CRFEntityExtractor"]
            # check to see if any of the possible CRFEntityExtractors will featurize `pattern`
            has_pattern_feature = False
            for crf in crf_components:
                crf_features = crf.component_config.get("features")
                # iterate through [[before],[word],[after]] features
                has_pattern_feature = "pattern" in itertools.chain(*crf_features)

            if not has_pattern_feature:
                raise_warning(
                    "You have defined training data consisting of lookup tables, but "
                    "your NLU pipeline's 'CRFEntityExtractor' does not include the 'pattern' feature. "
                    "To featurize lookup tables, add the 'pattern' feature to the 'CRFEntityExtractor' in "
                    "your pipeline."
                )

    if data.entity_synonyms and not any_components_in_pipeline(
        ["EntitySynonymMapper"], pipeline
    ):
        raise_warning(
            "You have defined synonyms in your training data, but "
            "your NLU pipeline does not include an 'EntitySynonymMapper'. "
            "To map synonyms, add an 'EntitySynonymMapper' to your pipeline."
        )


class MissingArgumentError(ValueError):
    """当不是所有参数都可以从 上下文/配置（context/config）中填充时引发

    Attributes:
        message -- 缺少那个参数的说明
    """

    def __init__(self, message: Text) -> None:
        super().__init__(message)
        self.message = message

    def __str__(self) -> Text:
        return self.message


class UnsupportedLanguageError(Exception):
    """在创建组件但不支持该语言时引发。

    Attributes:
        component -- 组件名称
        language -- 组件不支持的语言
    """

    def __init__(self, component: Text, language: Text) -> None:
        self.component = component
        self.language = language

        super().__init__(component, language)

    def __str__(self) -> Text:
        return (
            f"component '{self.component}' does not support language '{self.language}'."
        )


class ComponentMetaclass(type):
    """具有`name`类属性的元类（Metaclass with `name` class property）
    元类的用法"""

    @property
    def name(cls):
        """The name property is a function of the class - its __name__."""

        return cls.__name__


class Component(metaclass=ComponentMetaclass):
    """组件是管道中的消息处理单元：

    组件在管道中按照顺序排列。 每个组件都紧跟另一个。 这适用于初始化，训练，持久化和加载组件。
    如果组件首先出现在管道中，则将首先调用其方法。 例如：为了处理传入的消息，将调用每个组件的``process''方法。
    在处理（以及训练，持久和初始化）过程中，组件可以将信息传递给其他组件。通过为所谓的管道上下文体用的属性，
    将信息传递给其他组件。管道上下文包含当前组件的之前组件的所有信息，并用这些信息来执行自己的处理。
    例如，一个featurizer组件可以提供其他组件在管道中用来进行意图分类的功能。
    """

    # 组件类的名称是在管道中集成时使用。
    # 例如：“ [ComponentA，ComponentB]”将是正确的管道定义，其中“ComponentA”是管道的第一个组件的名称。
    @property
    def name(self):
        """从实例访问类的属性：名称。"""
        return type(self).name

    # 定义组件的默认配置参数，这些值可以在模型的管道配置中被覆盖。
    # 组件应选择合理的默认值，并且应该能够使用默认值创建合理的结果。
    defaults = {}

    # 定义此组件可以处理的语言。该属性是为实例方法“can_handle_language”设计的。
    # 默认值为None，这意味着它可以处理所有语言。这是组件向后兼容的重要功能。
    language_list = None

    def __init__(self, component_config: Optional[Dict[Text, Any]] = None) -> None:

        if not component_config:
            component_config = {}

        # 确保配置名称是配置的一部分（makes sure the name of the configuration is part of the config）
        # 这非常重要，对于例如：persistence
        component_config["name"] = self.name

        # 自定义配置会覆盖默认配置：自定义是在config.yml中定义的，默认配置是在组件中定义的defaults
        self.component_config = override_defaults(self.defaults, component_config)

        self.partial_processing_pipeline = None
        self.partial_processing_context = None

    # 该组件需要哪些组件支持：列出的组件在管道中应该出现该组件之前。
    @classmethod
    def required_components(cls) -> List[Type["Component"]]:
        """指定管道中需要存在哪些组件。
        Returns:
            所需组件的类名称的列表。
        """
        return []

    @classmethod
    def required_packages(cls) -> List[Text]:
        """指定需要安装的python软件包。
        例如：``[“spacy”]``。
        更具体地说，这些应该是可导入的python包名称，例如sklearn。
        而不是依赖关系中的软件包名称，例如`scikit-learn`。
        如果未安装必需的软件包，此需求列表会使我们能够在训练期间尽早失败。
        Returns:
            所需软件包名称的列表。
        """
        return []

    @classmethod
    def load(
        cls,
        meta: Dict[Text, Any],
        model_dir: Optional[Text] = None,
        model_metadata: Optional["Metadata"] = None,
        cached_component: Optional["Component"] = None,
        **kwargs: Any,
    ) -> "Component":
        """从文件加载此组件。
        训练完一个组件后，将通过调用“persist”将其保留。 当再次加载管道时，该组件需要能够恢复自身。
        组件可以依赖由components.Component.create创建的任何上下文属性，调用此组件之前的组件。
        Args:
            meta:           与模型相关的任何配置参数。
            model_dir:      要从中加载组件的目录。
            model_metadata: 模型的：class：`rasa.nlu.model.Metadata`。
            cached_component: 缓存的组件。
        Returns:
            加载的组件
        """
        if cached_component:
            return cached_component
        else:
            return cls(meta)

    @classmethod
    def create(
        cls, component_config: Dict[Text, Any], config: RasaNLUModelConfig
    ) -> "Component":
        """创建此组件（例如，在开始培训之前）。此方法可以访问所有配置参数
        Args:
            component_config:   组件配置参数。
            config:             模型配置参数。
        Returns:
            创建的组件。
        """

        # 检查语言支持
        language = config.language

        # 检查失败
        if not cls.can_handle_language(language):
            raise UnsupportedLanguageError(cls.name, language)

        return cls(component_config)

    def provide_context(self) -> Optional[Dict[Text, Any]]:
        """为新管道初始化此组件。

        在训练开始之前和使用解释器处理第一条消息之前，将调用此函数。
        该组件这个时候可以将信息添加到上下文中，并且上下文是在训练和消息解析期间通过管道传递的。
        大多数组件不需要实现此方法。 它主要用于初始化MITIE和spacy之类的框架环境（例如，为管道加载单词向量）。
        """
        pass

    def train(
        self,
        training_data: TrainingData,
        config: Optional[RasaNLUModelConfig] = None,
        **kwargs: Any,
    ) -> None:
        """训练此组件

        根据提供的训练数据，在这里进行组件的训练。
        该组件可以依赖于存在的任何上下文属性，这些上下文属性是通过调用ANY组件的rasa.nlu.components.Component.create`
        以及通过调用先前的组件: meth:`rasa.nlu.components.Component.train`创建的任意上下文属性。
        Args:
            training_data:  the :class:`rasa.nlu.training_data.training_data.TrainingData`.
            config:         模型配置参数。
        """

        pass

    def process(self, message: Message, **kwargs: Any) -> None:
        """处理传入的消息。

        在这里处理传入消息的组件。
        该组件可以依赖于存在的任何上下文属性，这些上下文属性是通过调用ANY组件的rasa.nlu.components.Component.create`
        以及通过调用先前的组件: meth:`rasa.nlu.components.Component.train`创建的任意上下文属性。

        The component can rely on any context attribute to be present, that gets created
        by a call to :meth:`rasa.nlu.components.Component.create` of ANY component
        and on any context attributes created by a call to :meth:`rasa.nlu.components.Component.process`
        of components previous to this one.

        Args:
            message: The :class:`rasa.nlu.training_data.message.Message` to process.
        """

        pass

    def persist(self, file_name: Text, model_dir: Text) -> Optional[Dict[Text, Any]]:
        """将该组件持久保存到磁盘以供将来加载。
        Args:
            file_name: 模型的文件名。
            model_dir: 将模型存储到的目录。
        Returns:
            可选字典，其中包含有关存储模型的一些信息。
        """

        pass

    @classmethod
    def cache_key(
        cls, component_meta: Dict[Text, Any], model_metadata: "Metadata"
    ) -> Optional[Text]:
        """该键（key）用于缓存组件。

        如果组件对于模型是唯一的，则应返回None。
        否则，该组件的实例化将会在元数据创建相同key的所有模型中被重用。

        If a component is unique to a model it should return None.
        Otherwise, an instantiation of the component will be reused
        for all models where the metadata creates the same key.

        Args:
            component_meta: 组件配置
            model_metadata: 组件的:class:`rasa.nlu.model.Metadata`.

        Returns:
            唯一的缓存key
        """

        return None

    def __getstate__(self) -> Any:
        """有些属性不应该被持久化处理（pickled）"""

        d = self.__dict__.copy()
        if "partial_processing_context" in d:
            del d["partial_processing_context"]
        if "partial_processing_pipeline" in d:
            del d["partial_processing_pipeline"]
        return d

    def __eq__(self, other) -> bool:
        return self.__dict__ == other.__dict__

    def prepare_partial_processing(
        self, pipeline: List["Component"], context: Dict[Text, Any]
    ) -> None:
        """设置用于部分处理的管道和上下文。

        pipeline应该是管道中该组件之前的组件列表，并且已经完成了它们的训练（因此可以安全地用于处理消息）。

        Args:
            pipeline: 组件列表
            context: 处理的上下文
        """
        self.partial_processing_pipeline = pipeline
        self.partial_processing_context = context

    def partially_process(self, message: Message) -> Message:
        """允许组件在训练期间处理消息（例如，外部训练数据）。

        传递的消息将由管道中该组件之前的所有组件处理。
        Args:
            message: 要处理的 :class:`rasa.nlu.training_data.message.Message`

        Returns:
            处理后的 :class:`rasa.nlu.training_data.message.Message`.
        """

        if self.partial_processing_context is not None:
            for component in self.partial_processing_pipeline:
                component.process(message, **self.partial_processing_context)
        else:
            logger.info("Failed to run partial processing due to missing pipeline.")
        return message

    @classmethod
    def can_handle_language(cls, language: Hashable) -> bool:
        """检查组件是否支持指定语言。

        需要时可以覆盖此方法。（例如，动态确定支持哪种语言。）
        Args:
            language: 要检查的语言。
        Returns:
            如果组件可以处理指定语言，则为True，否则为False。
        """

        # 如果language_list设置为“None”，则表示：支持所有语言
        if language is None or cls.language_list is None:
            return True

        return language in cls.language_list


class ComponentBuilder:
    """根据配置创建训练器和解释器。
        --缓存组件是为了重用。
    """

    def __init__(self, use_cache: bool = True) -> None:
        self.use_cache = use_cache

        # 尽可能重复使用nlp和featurizer，以节省内存，实现缓存键（cache-key）的每个组件都将被缓存。
        # 组件缓存的是大文件，用来节省内存和加速
        self.component_cache = {}

    def __get_cached_component(
        self, component_meta: Dict[Text, Any], model_metadata: "Metadata"
    ) -> Tuple[Optional[Component], Optional[Text]]:
        # 从缓存中加载组件（如果存在）。 返回组件（如果找到）和缓存键，如果没有找到，只返回缓存键。

        from rasa.nlu import registry

        # 尝试首先获取类名称，否则按名称创建
        component_name = component_meta.get("class", component_meta["name"])
        component_class = registry.get_component_class(component_name)

        # 根据缓存键（cache_key）：组件类名+模型路径
        cache_key = component_class.cache_key(component_meta, model_metadata)
        if (
            cache_key is not None
            and self.use_cache
            and cache_key in self.component_cache  # 判断cache_key是否存在于component_cache字典中
        ):
            return self.component_cache[cache_key], cache_key
        else:
            return None, cache_key

    def __add_to_cache(self, component: Component, cache_key: Optional[Text]) -> None:
        """添加组件到cache"""

        if cache_key is not None and self.use_cache:
            self.component_cache[cache_key] = component
            logger.info(
                f"Added '{component.name}' to component cache. Key '{cache_key}'."
            )

    def load_component(
        self,
        component_meta: Dict[Text, Any],
        model_dir: Text,
        model_metadata: "Metadata",
        **context: Any,
    ) -> Component:
        """加载组件。

        尝试从缓存中检索组件，否则调用“load”以创建新组件。
        Args:
            component_meta: 要在管道中加载的组件的元数据。
            model_dir:      从中读取模型的目录
            model_metadata (Metadata): 模型的：class：`rasa.nlu.model.Metadata`。
        Returns:
            加载的组件
        """

        from rasa.nlu import registry
        # 返回：组件类+缓存key
        try:
            cached_component, cache_key = self.__get_cached_component(
                component_meta, model_metadata
            )
            component = registry.load_component_by_meta(
                component_meta, model_dir, model_metadata, cached_component, **context
            )
            if not cached_component:
                self.__add_to_cache(component, cache_key)   # 如果该组件不在缓存中，让我们尽可能添加它
            return component
        except MissingArgumentError as e:  # pragma: no cover
            raise Exception(
                f"Failed to load component from file '{component_meta.get('file')}'. "
                f"Error: {e}"
            )

    def create_component(
        self, component_config: Dict[Text, Any], cfg: RasaNLUModelConfig
    ) -> Component:
        """创建一个组件。

        首先尝试从缓存中检索组件。
        如果缓存中没有该组件，就调用`create`创建该组件。

        Args:
            cache_key：存放需要加载的比较大的文件（与该组件配套）
            component_config:   组件配置。
            cfg:                模型配置。
        Returns:
            创建的组件
        """

        from rasa.nlu import registry
        from rasa.nlu.model import Metadata

        try:

            # 从缓存中查询缓存的cache_key，如果从缓存中查询到缓存的cache_key，但没有组件，
            # 就添加组件到缓存中，这里主要是缓存一些预训模型文件
            component, cache_key = self.__get_cached_component(
                component_config, Metadata(cfg.as_dict(), None)
            )

            # 如果查询到cache_key，但component为None，就把该组件加入到缓存字典中；
            # 在没有cache_key时，只返回该组件对象，不加入到缓存。
            if component is None:
                component = registry.create_component_by_config(component_config, cfg)
                self.__add_to_cache(component, cache_key)  # cache_key为空时，不添加到缓存中
            return component

        except MissingArgumentError as e:  # pragma: no cover
            raise Exception(
                f"Failed to create component '{component_config['name']}'. "
                f"Error: {e}"
            )
